查看原文
其他

Redis分布式锁的实现(Jedis和Redisson两个方案)

知识追寻者 Java笔记虾 2023-12-26

来源:https://my.oschina.net/u/3452433/blog/3004997/print

应用场景

分布式锁主要用于解决,公司中不同业务系统对同一功能的数据产生脏读或重复插入。

比如公司现有三个小组分别开发WAP站、小程序、APP客户端,而这三个系统都存在领红包功能。

业务要求每人每日只能领取一个红包,如果有人同时登陆三个系统那么就能够同一时间领取到三个红包。

分布式锁的要求

分布式锁要满足以下基本要求:

  1. 「共享锁」多系统能够共享同一个锁机制。
  2. 「互斥性」在任意时刻,只有一个请求能持有锁。
  3. 「无死锁」在程序崩溃时能够,自动释放锁。
  4. 「持有者解锁」锁只能被加锁的请求解锁,其他请求无法解锁。

Jedis实现分布式锁

本例参考了博文:https://wudashan.cn/2017/10/23/Redis-Distributed-Lock-Implement/

例子已上传码云:https://gitee.com/imlichao/jedis-distributed-lock-example

添加依赖

本例使用spring boot提供的redis实现,并没有直接引入jedis依赖。这样做的好处是,可以在项目中同时使用Jedis和RedisTemplate实例。

pom.xml文件

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-redis</artifactId>
</dependency>

配置文件

application.properties文件

#redis
spring.redis.database=0
spring.redis.host=18.6.8.22
spring.redis.password=Mfqy_redis_password_
spring.redis.port=6379
#连接超时时间(项目或连接池链接redis超时的时间)
spring.redis.timeout=2000
#最大连接数(建议为 业务期望QPS/单个连接的QPS,50000/1000=50)
spring.redis.pool.max-active=50
#最大空闲链接数(为减小伸缩产生的性能消耗,建议和最大连接数设成一致的)
spring.redis.pool.max-idle=50
#最小空闲连接数(0代表在无请求的状况下从不创建链接)
spring.redis.pool.min-idle=0
#连接池占满后无法获取连接时的阻塞时间(超时后抛出异常)
spring.redis.pool.max-wait=3000

Jedis工厂类

由于我们使用了spring boot提供的redis实现,所以我们不能直接获取到jedis对象。Jedis工厂类从RedisConnectionFactory中获取Redis连接(JedisConnection实现类),然后使用反射的方法从中取得了Jedis实例。

/**
 * Jedis工厂类(单例模式)
 */

@Service
public class JedisFactory {
    @Autowired
    private RedisConnectionFactory connectionFactory;

    private JedisFactory(){}

    private static Jedis jedis;
    /**
     *  获得jedis对象
     */

    public Jedis getJedis() {
        //从RedisConnectionFactory中获取Redis连接(JedisConnection实现类),然后使用反射的方法从中取得了Jedis实例
        if(jedis == null){
            Field jedisField = ReflectionUtils.findField(JedisConnection.class, "jedis");
            ReflectionUtils.makeAccessible(jedisField);
            jedis = (Jedis) ReflectionUtils.getField(jedisField, connectionFactory.getConnection());
        }
        return jedis;
    }
}

为避免死锁的发生,加锁和设定失效时间必须是一个原子性操作。否则一旦在加锁后程序出错,没能够执行设置失效时间的方法时,就会产生死锁。但是RedisTemplate屏蔽了插入数据和设置失效时间同时执行的方法,我们只能获取到Jedis实例来执行。

分布式锁实现类

分布式锁主要实现了两个方法即占用锁和释放锁。

这里需要注意占用锁和释放锁都要保证原子性操作,避免程序异常时产生死锁。

锁id主要用于标识持有锁的请求,在释放琐时用来判断只有持有正确锁id的请求才能执行解锁操作。

/**
 * redis分布式锁
 */

@Service
public class DistributedLock {

    @Autowired
    private JedisFactory JedisFactory;

    /**
     * 占用锁
     * @param lockKey 锁key
     * @return 锁id
     */

    public String occupyDistributedLock(String lockKey) {
        //获得jedis实例
        Jedis jedis = JedisFactory.getJedis();
        //锁id(必须拥有此id才能释放锁)
        String lockId = UUID.randomUUID().toString();
        //占用锁同时设置失效时间
        String isSuccees = jedis.set(lockKey, lockId, "NX","PX"15000);
        //占用锁成功返回锁id,否则返回null
        if("OK".equals(isSuccees)){
            return lockId;
        }else{
            return null;
        }
    }

    /**
     * 释放锁
     * @param lockKey 锁key
     * @param lockId 加锁id
     */

    public void releaseDistributedLock(String lockKey,String lockId) {
        if(lockId != null){
            //获得jedis实例
            Jedis jedis = JedisFactory.getJedis();
            //执行Lua代码删除lockId匹配的锁
            String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
            jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(lockId));
        }
    }
}

解释一下jedis.set(lockKey, lockId, "NX","PX", 15000)方法

格式 - String set(String key, String value, String nxxx, String expx, long time); 功能 - 存储数据到缓存中,并制定过期时间和当Key存在时是否覆盖。参数 -  key :redis key value :redis值 nxxx:只能取NX或者XX,如果取NX,则只有当key不存在是才进行set,如果取XX,则只有当key已经存在时才进行set expx:只能取EX或者PX,代表数据过期时间的单位,EX代表秒,PX代表毫秒。time:过期时间,单位是expx所代表的单位。

测试代码

「Controller」

/**
 * 分布式锁测试类
 */

@Controller
public class DistributedLockController {
    @Autowired
    private DistributedLock distributedLock;

    @RequestMapping(value = "/", method = RequestMethod.GET)
    public String index(){
        return "/index";
    }

    @RequestMapping(value = "/occupyDistributedLock", method = RequestMethod.GET)
    public String occupyDistributedLock(RedirectAttributes redirectAttributes, HttpServletRequest request){
        String key = "userid:55689";

        String lockId = null;
        try{
            //占用锁
            lockId = distributedLock.occupyDistributedLock(key);
            if(lockId != null){
                //程序执行
                TimeUnit.SECONDS.sleep(10);
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            //释放锁
            distributedLock.releaseDistributedLock(key,lockId);
        }
        redirectAttributes.addFlashAttribute("lockId",lockId);

        return "redirect:/";
    }
}

「页面」

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>distributed lock</title>
</head>
<body>
<h1>分布式锁测试</h1>
<button onclick="window.location.href = '/occupyDistributedLock'">占用锁</button>
<br/>
<!-- 占用成功返回锁id -->
<#if lockId??>${lockId}</#if>
</body>
</html>

Redisson实现分布式锁(推荐)

使用Redisson提供的分布式锁更加方便,而且锁的具体细节也不需要考虑。

例子已上传码云:https://gitee.com/imlichao/redisson-distributed-lock-example

官网:https://redisson.org/

文档:https://github.com/redisson/redisson/wiki

SpringBoot配置

「添加依赖」

spring boot 中引用专用依赖,会自动生成配置和spring bean的实例。

pom.xml文件

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.10.1</version>
</dependency>

「配置文件」

application.properties文件

#redisson
spring.redis.database=0
spring.redis.host=13.6.8.1
spring.redis.password=Mfqy_redis
spring.redis.port=6379

「测试代码」

「Controller」

/**
 * 分布式锁测试类
 */

@Controller
public class DistributedLockController {
    @Autowired
    private RedissonClient redisson ;

    @RequestMapping(value = "/", method = RequestMethod.GET)
    public String index(){
        return "/index";
    }

    @RequestMapping(value = "/occupyDistributedLock", method = RequestMethod.GET)
    public String occupyDistributedLock(RedirectAttributes redirectAttributes){
        RLock lock = null;
        try{
            //锁的key
            String key = "MF:DISTRIBUTEDLOCK:S:personId_1001";
            //获得分布式锁实例
            lock = redisson.getLock(key);
            //加锁并且设置自动失效时间15秒
            lock.lock(15, TimeUnit.SECONDS);
            //程序执行
            TimeUnit.SECONDS.sleep(10);
            //获取网络时间(多服务器测试统一时间)
            URL url=new URL("http://www.baidu.com");
            URLConnection conn=url.openConnection();
            conn.connect();
            long dateL=conn.getDate();
            Date date=new Date(dateL);
            //打印和返回结果
            System.out.println(date);
            redirectAttributes.addFlashAttribute("success",date);
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            //释放锁
            if (lock != null) lock.unlock();
        }

        return "redirect:/";
    }
}

「页面」

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>distributed lock</title>
</head>
<body>
<h1>分布式锁测试</h1>
<button onclick="window.location.href = '/occupyDistributedLock'">占用锁</button>
<br/>
<#if success??>${success?datetime}</#if>
</body>
</html>

手动配置

「添加依赖」

pom.xml文件

<dependency>
     <groupId>org.redisson</groupId>
     <artifactId>redisson</artifactId>
     <version>3.10.1</version>
</dependency>

「配置文件」

application.properties文件

#redisson
spring.redis.database=0
spring.redis.host=13.6.8.1
spring.redis.password=Mfqy_redis
spring.redis.port=6379

「配置类」

/**
 * Redisson配置
 */

@Configuration
public class RedissonConfig {

    @Value("${spring.redis.database}")
    private int database;

    @Value("${spring.redis.host}")
    private String host;

    @Value("${spring.redis.password}")
    private String password;

    @Value("${spring.redis.port}")
    private String port;

    @Bean
    RedissonClient createConfig() {
        Config config = new Config();
        //设置编码方式为 Jackson JSON 编码(不设置默认也是这个)
        config.setCodec(new JsonJacksonCodec());
        //云托管模式设置(我们公司用的阿里云redis产品)
        config.useReplicatedServers()
                //节点地址设置
                .addNodeAddress("redis://"+host+":"+port)
                //密码
                .setPassword(password)
                //数据库编号(默认0)
                .setDatabase(database);

        RedissonClient redisson = Redisson.create(config);
        return redisson;
    }
}

「测试代码与SpringBoot配置一样」

精彩推荐

SpringBoot内容聚合
IntelliJ IDEA内容聚合
Mybatis内容聚合



欢迎长按下图关注公众号后端技术精选

继续滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存